import { writeFileSync, copyFileSync, readFileSync, existsSync } from "fs";
import { readFile, writeFile } from "fs/promises";
import { join, basename, resolve, dirname } from "path";
import { exec, execSync } from "child_process";
import fetch from "node-fetch";
import { createRequire } from "module";
import { finished } from "stream/promises";

import gulp from "gulp";
import gulpTap from "gulp-tap";
import gulpZip from "gulp-zip";
import gulpRename from "gulp-rename";
import gulpReplace from "gulp-replace";
import { globby } from "globby";
import { rimraf } from "rimraf";
import { mkdirp } from "mkdirp";
import yargs from "yargs";
import typeScript from "typescript";
import { build as esbuild } from "esbuild";

import {
  buildBarium,
  buildEngine,
  buildWidgets,
  bundleWorkers,
  defaultESBuildOptions,
} from "./scripts/build.js";

// Determines the scope of the workspace packages. If the scope is set to barium, the workspaces should be @barium/engine.
// This should match the scope of the dependencies of the root level package.json.
const scope = "barium";

const require = createRequire(import.meta.url);
const packageJson = require("./package.json");
let version = packageJson.version;
if (/\.0$/.test(version)) {
  version = version.substring(0, version.length - 2);
}
const devDeployUrl = "https://ci-builds.barium.com/barium/";
const isProduction = process.env.PROD;

//Gulp doesn't seem to have a way to get the currently running tasks for setting
//per-task variables.  We use the command line argument here to detect which task is being run.
const taskName = process.argv[2];
const noDevelopmentGallery =
  taskName === "release" ||
  taskName === "makeZip" ||
  taskName === "websiteRelease";
const argv = yargs(process.argv).argv;

const sourceFiles = [
  "packages/engine/Source/**/*.js",
  "!packages/engine/Source/*.js",
  "packages/widgets/Source/**/*.js",
  "!packages/widgets/Source/*.js",
  "!packages/engine/Source/Shaders/**"
];
const shaderFiles = [
  "packages/engine/Source/Shaders/**/*.glsl"
];

// Print an esbuild warning
function printBuildWarning({ location, text }) {
  const { column, file, line, lineText, suggestion } = location;

  let message = `\n
  > ${file}:${line}:${column}: warning: ${text}
  ${lineText}
  `;

  if (suggestion && suggestion !== "") {
    message += `\n${suggestion}`;
  }

  console.log(message);
}

// Ignore `eval` warnings in third-party code we don't have control over
function handleBuildWarnings(result) {
  for (const warning of result.warnings) {
    if (
      !warning.location.file.includes("protobufjs.js") &&
      !warning.location.file.includes("Build/Barium")
    ) {
      printBuildWarning(warning);
    }
  }
}

// 复制必要文件至Build
function copyNeededFiles() {
  // Copy Draco3D files from node_modules into Source
  copyFileSync(
    "node_modules/draco3d/draco_decoder.wasm",
    "packages/engine/Source/ThirdParty/draco_decoder.wasm",
  );
}

export async function build() {
  // Configure build options from command line arguments.
  const minify = argv.minify ?? false;
  const removePragmas = argv.pragmas ?? false;
  const sourcemap = argv.sourcemap ?? true;
  const node = argv.node ?? true;

  const buildOptions = {
    development: !noDevelopmentGallery,
    iife: true,
    minify: minify,
    removePragmas: removePragmas,
    sourcemap: sourcemap,
    node: node,
  };

  // Configure build target.
  const workspace = argv.workspace ? argv.workspace : undefined;

  copyNeededFiles();

  if (workspace === `@${scope}/engine`) {
    return buildEngine(buildOptions);
  } else if (workspace === `@${scope}/widgets`) {
    return buildWidgets(buildOptions);
  }

  await buildEngine(buildOptions);
  await buildWidgets(buildOptions);
  await buildBarium(buildOptions);
}
export default build;

export const buildWatch = gulp.series(build, async function buildWatch() {
  const minify = argv.minify ? argv.minify : false;
  const removePragmas = argv.pragmas ? argv.pragmas : false;
  const sourcemap = argv.sourcemap ? argv.sourcemap : true;

  const outputDirectory = join("Build", `Barium${!minify ? "Unminified" : ""}`);

  const bundles = await buildBarium({
    minify: minify,
    path: outputDirectory,
    removePragmas: removePragmas,
    sourcemap: sourcemap,
    incremental: true,
  });

  const esm = bundles.esm;
  const cjs = bundles.node;
  const iife = bundles.iife;
  const specs = bundles.specs;

  gulp.watch(shaderFiles, async () => {
    glslToJavaScript(minify, "Build/minifyShaders.state", "engine");
    await esm.rebuild();

    if (iife) {
      await iife.rebuild();
    }

    if (cjs) {
      await cjs.rebuild();
    }
  });

  gulp.watch(
    [
      ...sourceFiles,
      // Shader results are generated in the previous watch task; no need to rebuild twice
      "!Source/Shaders/**",
    ],
    async () => {
      await esm.rebuild();

      if (iife) {
        await iife.rebuild();
      }

      if (cjs) {
        await cjs.rebuild();
      }

      await bundleWorkers({
        minify: minify,
        path: outputDirectory,
        removePragmas: removePragmas,
        sourcemap: sourcemap,
      });
    }
  );

  process.on("SIGINT", () => {
    // Free up resources
    esm.dispose();

    if (iife) {
      iife.dispose();
    }

    if (cjs) {
      cjs.dispose();
    }

    specs.dispose();

    process.exit(0);
  });
});

export async function buildTs() {
  let workspaces;
  if (argv.workspace && !Array.isArray(argv.workspace)) {
    workspaces = [argv.workspace];
  } else if (argv.workspace) {
    workspaces = argv.workspace;
  } else {
    workspaces = packageJson.workspaces;
  }

  // Generate types for passed packages in order.
  const importModules = {};
  for (const workspace of workspaces) {
    const directory = workspace
      .replace(`@${scope}/`, "")
      .replace(`packages/`, "");
    const workspaceModules = await generateTypeScriptDefinitions(
      directory,
      `packages/${directory}/index.d.ts`,
      `packages/${directory}/tsd-conf.json`,
      // The engine package needs additional processing for its enum strings
      directory === "engine" ? processEngineSource : undefined,
      // Handle engine's module naming exceptions
      directory === "engine" ? processEngineModules : undefined,
      importModules
    );
    importModules[directory] = workspaceModules;
  }

  if (argv.workspace) {
    return;
  }

  // Generate types for BariumJS.
  await createTypeScriptDefinitions();
}

export function buildApps() {
  return Promise.all([buildBariumViewer(), buildSandcastle()]);
}

const filesToClean = [
  "Source/Barium.js",
  "Source/Shaders/**/*.js",
  "Source/**/*.d.ts",
  "Apps/Sandcastle/jsHintOptions.js",
  "Apps/Sandcastle/gallery/gallery-index.js",
  "Apps/Sandcastle/templates/bucket.css",
  "Barium-*.zip",
  "barium-*.tgz",
  "packages/**/*.tgz",
];

export async function clean() {
  await rimraf("Build");
  const files = await globby(filesToClean);
  return Promise.all(files.map((file) => rimraf(file)));
}

//Builds the documentation
export async function buildDocs() {
  const generatePrivateDocumentation = argv.private ? "--private" : "";

  execSync(
    `npx jsdoc --configure Tools/jsdoc/conf.json --pedantic ${generatePrivateDocumentation}`,
    {
      stdio: "inherit",
      env: Object.assign({}, process.env, {
        BARIUM_VERSION: version,
        BARIUM_PACKAGES: packageJson.workspaces,
      }),
    },
  );

  const stream = gulp
    .src(["Documentation/Images/**"], { encoding: false })
    .pipe(gulp.dest("Build/Documentation/Images"));

  await finished(stream);
  return stream;
}

export async function buildDocsWatch() {
  await buildDocs();
  console.log("Listening for changes in documentation...");
  return gulp.watch(sourceFiles, buildDocs);
}

function combineForSandcastle() {
  const outputDirectory = join("Build", "Sandcastle", "BariumUnminified");
  return buildBarium({
    development: false,
    minify: false,
    removePragmas: false,
    node: false,
    outputDirectory: outputDirectory,
  });
}

export const websiteRelease = gulp.series(
  buildEngine,
  buildWidgets,
  function websiteReleaseBuild() {
    return buildBarium({
      development: false,
      minify: false,
      removePragmas: false,
      node: false,
    });
  },
  combineForSandcastle,
  buildDocs
);

export const buildRelease = gulp.series(
  buildEngine,
  buildWidgets,
  // Generate Build/BariumUnminified
  function buildBariumForNode() {
    copyNeededFiles();
    return buildBarium({
      minify: false,
      removePragmas: false,
      node: true,
      sourcemap: false,
    });
  },
  // Generate Build/Barium
  function buildMinifiedBariumForNode() {
    copyNeededFiles();
    return buildBarium({
      development: false,
      minify: true,
      removePragmas: true,
      node: true,
      sourcemap: false,
    });
  }
);

export const release = gulp.series(
  buildRelease,
  gulp.parallel(buildTs, buildDocs)
);

export const postversion = async function () {
  const workspace = argv.workspace;
  if (!workspace) {
    return;
  }
  const directory = workspace.replaceAll(`@${scope}/`, ``);
  const workspacePackageJson = require(`./packages/${directory}/package.json`);
  const version = workspacePackageJson.version;

  // Iterate through all package JSONs that may depend on the updated package and
  // update the version of the updated workspace.
  const packageJsons = await globby([
    "./package.json",
    "./packages/*/package.json",
  ]);
  const promises = packageJsons.map(async (packageJsonPath) => {
    // Ensure that we don't check the updated workspace itself.
    if (basename(dirname(packageJsonPath)) === directory) {
      return;
    }
    // Ensure that we only update workspaces where the dependency to the updated workspace already exists.
    const packageJson = require(packageJsonPath);
    if (!Object.hasOwn(packageJson.dependencies, workspace)) {
      console.log(
        `Skipping update for ${workspace} as it is not a dependency.`
      );
      return;
    }
    // Update the version for the updated workspace.
    packageJson.dependencies[workspace] = `^${version}`;
    await writeFile(packageJsonPath, JSON.stringify(packageJson, undefined, 2));
  });
  return Promise.all(promises);
};

/**
 * Removes scripts from package.json files to ensure that
 * they still work when run from within the ZIP file.
 *
 * @param {string} packageJsonPath The path to the package.json.
 * @returns {WritableStream} A stream that writes to the updated package.json file.
 */
async function pruneScriptsForZip(packageJsonPath) {
  // Read the contents of the file.
  const contents = await readFile(packageJsonPath);
  const contentsJson = JSON.parse(contents);

  const scripts = contentsJson.scripts;

  // Remove build and transform tasks since they do not function as intended from within the release zip
  delete scripts.build;
  delete scripts["build-release"];
  delete scripts["build-watch"];
  delete scripts["build-ts"];
  delete scripts["build-third-party"];
  delete scripts["build-apps"];
  delete scripts.clean;
  delete scripts["build-docs"];
  delete scripts["build-docs-watch"];
  delete scripts["make-zip"];
  delete scripts.release;
  delete scripts.prettier;

  // Remove deploy tasks
  delete scripts["deploy-status"];
  delete scripts["deploy-set-version"];
  delete scripts["website-release"];

  // Set server tasks to use production flag
  scripts["start"] = "node server.js --production";
  scripts["start-public"] = "node server.js --public --production";
  scripts["start-public"] = "node server.js --public --production";
  scripts["test"] = "gulp test --production";
  scripts["test-all"] = "gulp test --all --production";
  scripts["test-webgl"] = "gulp test --include WebGL --production";
  scripts["test-non-webgl"] = "gulp test --exclude WebGL --production";
  scripts["test-webgl-validation"] = "gulp test --webglValidation --production";
  scripts["test-webgl-stub"] = "gulp test --webglStub --production";
  scripts["test-release"] = "gulp test --release --production";

  // Write to a temporary package.json file.
  const noPreparePackageJson = join(
    dirname(packageJsonPath),
    "package.noprepare.json"
  );
  await writeFile(noPreparePackageJson, JSON.stringify(contentsJson, null, 2));

  return gulp.src(noPreparePackageJson, {
    base: ".",
  });
}

export const makeZip = gulp.series(release, async function createZipFile() {
  //For now we regenerate the JS glsl to force it to be unminified in the release zip
  //See https://github.com/BariumGS/barium/pull/3106#discussion_r42793558 for discussion.
  await glslToJavaScript(false, "Build/minifyShaders.state", "engine");

  const packageJsonSrc = await pruneScriptsForZip("package.json");
  const enginePackageJsonSrc = await pruneScriptsForZip(
    "packages/engine/package.json"
  );
  const widgetsPackageJsonSrc = await pruneScriptsForZip(
    "packages/widgets/package.json"
  );

  const src = gulp
    .src("index.release.html")
    .pipe(
      gulpRename((file) => {
        if (file.basename === "index.release") {
          file.basename = "index";
        }
      })
    )
    .pipe(enginePackageJsonSrc)
    .pipe(widgetsPackageJsonSrc)
    .pipe(packageJsonSrc)
    .pipe(
      gulpRename((file) => {
        if (file.basename === "package.noprepare") {
          file.basename = "package";
        }
      })
    )
    .pipe(
      gulp.src(
        [
          "Build/Barium/**",
          "Build/BariumUnminified/**",
          "Build/Docs/**",
          "Build/package.json",
          "packages/engine/Build/**",
          "packages/widgets/Build/**",
          "!Build/InlineWorkers.js",
          "!packages/engine/Build/minifyShaders.state",
        ],
        {
          encoding: false,
          base: ".",
        }
      )
    )
    .pipe(
      gulp.src(
        [
          "Apps/**",
          "packages/engine/index.js",
          "packages/engine/index.d.ts",
          "packages/engine/LICENSE.md",
          "packages/engine/README.md",
          "packages/engine/Source/**",
          "packages/widgets/index.js",
          "packages/widgets/index.d.ts",
          "packages/widgets/LICENSE.md",
          "packages/widgets/README.md",
          "packages/widgets/Source/**",
          "Source/**",
          "scripts/**",
          "favicon.ico",
          ".prettierignore",
          "eslint.config.js",
          "gulpfile.js",
          "server.js",
          "index.cjs",
          "LICENSE.md",
          "CHANGES.md",
          "README.md",
          "web.config",
          "!**/*.gitignore",
          "!Apps/Sandcastle/gallery/development/**",
        ],
        {
          encoding: false,
          base: ".",
        }
      )
    )
    .pipe(
      gulpTap(function (file) {
        // Work around an issue with gulp-zip where archives generated on Windows do
        // not properly have their directory executable mode set.
        // see https://github.com/sindresorhus/gulp-zip/issues/64#issuecomment-205324031
        if (file.isDirectory()) {
          file.stat.mode = parseInt("40777", 8);
        }
      })
    )
    .pipe(gulpZip(`Barium-${version}.zip`))
    .pipe(gulp.dest("."));

  await finished(src);

  rimraf.sync("./package.noprepare.json");
  rimraf.sync("./packages/engine/package.noprepare.json");
  rimraf.sync("./packages/widgets/package.noprepare.json");

  return src;
});

export async function deploySetVersion() {
  const buildVersion = argv.buildVersion;
  if (buildVersion) {
    // NPM versions can only contain alphanumeric and hyphen characters
    packageJson.version += `-${buildVersion.replace(/[^[0-9A-Za-z-]/g, "")}`;
    return writeFile("package.json", JSON.stringify(packageJson, undefined, 2));
  }
}

export async function deployStatus() {
  const status = argv.status;
  const message = argv.message;

  const deployUrl = `${devDeployUrl + process.env.BRANCH}/`;
  const zipUrl = `${deployUrl}Barium-${version}.zip`;
  const npmUrl = `${deployUrl}barium-${version}.tgz`;
  const coverageUrl = `${
    devDeployUrl + process.env.BRANCH
  }/Build/Coverage/index.html`;

  return Promise.all([
    setStatus(status, deployUrl, message, "deployment"),
    setStatus(status, zipUrl, message, "zip file"),
    setStatus(status, npmUrl, message, "npm package"),
    setStatus(status, coverageUrl, message, "coverage results"),
  ]);
}

async function setStatus(state, targetUrl, description, context) {
  // skip if the environment does not have the token
  if (!process.env.GITHUB_TOKEN) {
    return;
  }

  const body = {
    state: state,
    target_url: targetUrl,
    description: description,
    context: context,
  };

  const response = await fetch(
    `https://api.github.com/repos/${process.env.GITHUB_REPO}/statuses/${process.env.GITHUB_SHA}`,
    {
      method: "post",
      body: JSON.stringify(body),
      headers: {
        "Content-Type": "application/json",
        Authorization: `token ${process.env.GITHUB_TOKEN}`,
        "User-Agent": "Barium",
      },
    }
  );

  const result = await response.json();
  return result;
}

/**
 * Generates TypeScript definition file (.d.ts) for a package.
 *
 * @param {*} workspaceName
 * @param {string} definitionsPath The path of the .d.ts file to generate.
 * @param {*} configurationPath
 * @param {*} processSourceFunc
 * @param {*} processModulesFunc
 * @param {*} importModules
 * @returns
 */
function generateTypeScriptDefinitions(
  workspaceName,
  definitionsPath,
  configurationPath,
  processSourceFunc,
  processModulesFunc,
  importModules
) {
  // Run JSDoc with tsd-jsdoc to generate an initial definition file.
  execSync(`npx jsdoc --configure ${configurationPath}`, {
    stdio: `inherit`,
  });

  let source = readFileSync(definitionsPath).toString();

  if (processSourceFunc) {
    source = processSourceFunc(definitionsPath, source);
  }

  // The next step is to find the list of Barium modules exported by the Barium API
  // So that we can map these modules with a link back to their original source file.

  const regex = /^declare[ const ]*(function|class|namespace|enum) (.+)/gm;
  let matches;
  let publicModules = new Set();

  while ((matches = regex.exec(source))) {
    const moduleName = matches[2].match(/([^<\s|\(]+)/);
    publicModules.add(moduleName[1]);
  }

  if (processModulesFunc) {
    publicModules = processModulesFunc(publicModules);
  }

  // Fix up the output to match what we need
  // declare => export since we are wrapping everything in a namespace
  // BariumMath => Math (because no BariumJS build step would be complete without special logic for the Math class)
  // Fix up the WebGLConstants aliasing we mentioned above by simply unquoting the strings.
  source = source
    .replace(/^declare /gm, "export ")
    .replace(/module "Math"/gm, "namespace Math")
    .replace(/BariumMath/gm, "Math")
    .replace(/Number\[]/gm, "number[]") // Workaround https://github.com/englercj/tsd-jsdoc/issues/117
    .replace(/String\[]/gm, "string[]")
    .replace(/Boolean\[]/gm, "boolean[]")
    .replace(/Object\[]/gm, "object[]")
    .replace(/<Number>/gm, "<number>")
    .replace(/<String>/gm, "<string>")
    .replace(/<Boolean>/gm, "<boolean>")
    .replace(/<Object>/gm, "<object>")
    .replace(
      /= "WebGLConstants\.(.+)"/gm,
      // eslint-disable-next-line no-unused-vars
      (match, p1) => `= WebGLConstants.${p1}`
    )
    // Strip const enums which can cause errors - https://www.typescriptlang.org/docs/handbook/enums.html#const-enum-pitfalls
    .replace(/^(\s*)(export )?const enum (\S+) {(\s*)$/gm, "$1$2enum $3 {$4")
    // Replace JSDoc generation version of defined with an improved version using TS type predicates
    .replace(
      /defined\(value: any\): boolean/gm,
      "defined<Type>(value: Type): value is NonNullable<Type>"
    )
    .replace(
      /\/\*\*[\*\s\w]*?\*\/\nexport const Check: any;/m,
      `\n${readFileSync("./packages/engine/Source/Core/Check.d.ts").toString()}`
    )
    // Fix https://github.com/BariumGS/barium/issues/10498 so we can use the rest parameter expand tuple
    .replace(
      "raiseEvent(...arguments: Parameters<Listener>[]): void;",
      "raiseEvent(...arguments: Parameters<Listener>): void;"
    );

  // Wrap the source to actually be inside of a declared barium module
  // and add any workaround and private utility types.
  source = `declare module "@${scope}/${workspaceName}" {
${source}
}
`;

  if (importModules) {
    let imports = "";
    Object.keys(importModules).forEach((workspace) => {
      const workspaceModules = Array.from(importModules[workspace]).filter(
        (importModule) => source.indexOf(importModule) !== -1
      );
      imports += `import { ${workspaceModules.join(
        ",\n"
      )} } from "@${scope}/${workspace}";\n`;
    });
    source = imports + source;
  }

  // Write the final source file back out
  writeFileSync(definitionsPath, source);

  return Promise.resolve(publicModules);
}

function processEngineModules(modules) {
  // Math shows up as "Math" because of it's aliasing from BariumMath and namespace collision with actual Math
  // It fails the above regex so just add it directly here.
  modules.add("Math");
  return modules;
}

function processEngineSource(definitionsPath, source) {
  // All of our enum assignments that alias to WebGLConstants, such as PixelDatatype.js
  // end up as enum strings instead of actually mapping values to WebGLConstants.
  // We fix this with a simple regex replace later on, but it means the
  // WebGLConstants constants enum needs to be defined in the file before it can
  // be used.  This block of code reads in the TS file, finds the WebGLConstants
  // declaration, and then writes the file back out (in memory to source) with
  // WebGLConstants being the first module.
  const node = typeScript.createSourceFile(
    definitionsPath,
    source,
    typeScript.ScriptTarget.Latest
  );
  let firstNode;
  node.forEachChild((child) => {
    if (
      typeScript.SyntaxKind[child.kind] === "EnumDeclaration" &&
      child.name.escapedText === "WebGLConstants"
    ) {
      firstNode = child;
    }
  });

  const printer = typeScript.createPrinter({
    removeComments: false,
    newLine: typeScript.NewLineKind.LineFeed,
  });

  let newSource = "";
  newSource += printer.printNode(
    typeScript.EmitHint.Unspecified,
    firstNode,
    node
  );
  newSource += "\n\n";
  node.forEachChild((child) => {
    if (
      typeScript.SyntaxKind[child.kind] !== "EnumDeclaration" ||
      child.name.escapedText !== "WebGLConstants"
    ) {
      newSource += printer.printNode(
        typeScript.EmitHint.Unspecified,
        child,
        node
      );
      newSource += "\n\n";
    }
  });

  // Manually add a type definition from Viewer to avoid circular dependency
  // with the widgets package. This will no longer be needed past Barium 1.100.
  newSource += `
  /**
   * @property scene - The scene in the widget.
   */
  export type Viewer = {
      scene: Scene;
  };
  `;

  return newSource;
}

function createTypeScriptDefinitions() {
  // Run jsdoc with tsd-jsdoc to generate an initial Barium.d.ts file.
  let source = readFileSync("Source/Barium.d.ts").toString();

  // All of our enum assignments that alias to WebGLConstants, such as PixelDatatype.js
  // end up as enum strings instead of actually mapping values to WebGLConstants.
  // We fix this with a simple regex replace later on, but it means the
  // WebGLConstants constants enum needs to be defined in the file before it can
  // be used.  This block of code reads in the TS file, finds the WebGLConstants
  // declaration, and then writes the file back out (in memory to source) with
  // WebGLConstants being the first module.
  const node = typeScript.createSourceFile(
    "Source/Barium.d.ts",
    source,
    typeScript.ScriptTarget.Latest
  );
  let firstNode;
  node.forEachChild((child) => {
    if (
      typeScript.SyntaxKind[child.kind] === "EnumDeclaration" &&
      child.name.escapedText === "WebGLConstants"
    ) {
      firstNode = child;
    }
  });

  const printer = typeScript.createPrinter({
    removeComments: false,
    newLine: typeScript.NewLineKind.LineFeed,
  });

  let newSource = "";
  newSource += printer.printNode(
    typeScript.EmitHint.Unspecified,
    firstNode,
    node
  );
  newSource += "\n\n";
  node.forEachChild((child) => {
    if (
      typeScript.SyntaxKind[child.kind] !== "EnumDeclaration" ||
      child.name.escapedText !== "WebGLConstants"
    ) {
      newSource += printer.printNode(
        typeScript.EmitHint.Unspecified,
        child,
        node
      );
      newSource += "\n\n";
    }
  });
  source = newSource;

  // The next step is to find the list of Barium modules exported by the Barium API
  // So that we can map these modules with a link back to their original source file.

  const regex = /^declare (function|class|namespace|enum) (.+)/gm;
  let matches;
  const publicModules = new Set();

  while ((matches = regex.exec(source))) {
    const moduleName = matches[2].match(/([^<\s|\(]+)/);
    publicModules.add(moduleName[1]);
  }

  // Math shows up as "Math" because of it's aliasing from BariumMath and namespace collision with actual Math
  // It fails the above regex so just add it directly here.
  publicModules.add("Math");

  // Fix up the output to match what we need
  // declare => export since we are wrapping everything in a namespace
  // BariumMath => Math (because no BariumJS build step would be complete without special logic for the Math class)
  // Fix up the WebGLConstants aliasing we mentioned above by simply unquoting the strings.
  source = source
    .replace(/^declare /gm, "export ")
    .replace(/module "Math"/gm, "namespace Math")
    .replace(/BariumMath/gm, "Math")
    .replace(/Number\[]/gm, "number[]") // Workaround https://github.com/englercj/tsd-jsdoc/issues/117
    .replace(/String\[]/gm, "string[]")
    .replace(/Boolean\[]/gm, "boolean[]")
    .replace(/Object\[]/gm, "object[]")
    .replace(/<Number>/gm, "<number>")
    .replace(/<String>/gm, "<string>")
    .replace(/<Boolean>/gm, "<boolean>")
    .replace(/<Object>/gm, "<object>")
    .replace(
      /= "WebGLConstants\.(.+)"/gm,
      // eslint-disable-next-line no-unused-vars
      (match, p1) => `= WebGLConstants.${p1}`
    )
    // Strip const enums which can cause errors - https://www.typescriptlang.org/docs/handbook/enums.html#const-enum-pitfalls
    .replace(/^(\s*)(export )?const enum (\S+) {(\s*)$/gm, "$1$2enum $3 {$4")
    // Replace JSDoc generation version of defined with an improved version using TS type predicates
    .replace(
      /defined\(value: any\): boolean/gm,
      "defined<Type>(value: Type): value is NonNullable<Type>"
    )
    .replace(
      /\/\*\*[\*\s\w]*?\*\/\nexport const Check: any;/m,
      `\n${readFileSync("./packages/engine/Source/Core/Check.d.ts").toString()}`
    )
    // Fix https://github.com/BariumGS/barium/issues/10498 to have rest parameter expand tuple
    .replace(
      "raiseEvent(...arguments: Parameters<Listener>[]): void;",
      "raiseEvent(...arguments: Parameters<Listener>): void;"
    );

  // Wrap the source to actually be inside of a declared barium module
  // and add any workaround and private utility types.
  source = `declare module "barium" {
${source}
}

`;

  // Write the final source file back out
  writeFileSync("Source/Barium.d.ts", source);

  return Promise.resolve();
}

async function buildSandcastle() {
  const streams = [];
  let appStream = gulp.src(
    [
      "Apps/Sandcastle/**",
      "!Apps/Sandcastle/load-barium-es6.js",
      "!Apps/Sandcastle/images/**",
      "!Apps/Sandcastle/gallery/**.jpg",
    ],
    {
      encoding: false,
    }
  );

  if (isProduction) {
    // Remove swap out ESM modules for the IIFE build
    appStream = appStream
      .pipe(
        gulpReplace(
          '    <script type="module" src="../load-barium-es6.js"></script>',
          '    <script src="../BariumUnminified/Barium.js"></script>\n' +
            '    <script>window.BARIUM_BASE_URL = "../BariumUnminified/";</script>'
        )
      )
      .pipe(
        gulpReplace(
          '    <script type="module" src="load-barium-es6.js"></script>',
          '    <script src="BariumUnminified/Barium.js"></script>\n' +
            '    <script>window.BARIUM_BASE_URL = "BariumUnminified/";</script>'
        )
      )
      // Fix relative paths for new location
      .pipe(gulpReplace("../../../Build", ".."))
      .pipe(gulpReplace("../../../Source", "../BariumUnminified"))
      .pipe(gulpReplace("../../Source", "."))
      .pipe(gulpReplace("../Apps/Sandcastle", "."))
      .pipe(gulpReplace("../../SampleData", "../SampleData"))
      .pipe(
        gulpReplace("../../Build/Docs", "/learn/BARIUMjs/ref-doc/")
      )
      .pipe(gulp.dest("Build/Sandcastle"));
  } else {
    // Remove swap out ESM modules for the IIFE build
    appStream = appStream
      .pipe(
        gulpReplace(
          '    <script type="module" src="../load-barium-es6.js"></script>',
          '    <script src="../../../Build/BariumUnminified/Barium.js"></script>\n' +
            '    <script>window.BARIUM_BASE_URL = "../../../Build/BariumUnminified/";</script>'
        )
      )
      .pipe(
        gulpReplace(
          '    <script type="module" src="load-barium-es6.js"></script>',
          '    <script src="../../BariumUnminified/Barium.js"></script>\n' +
            '    <script>window.BARIUM_BASE_URL = "../../BariumUnminified/";</script>'
        )
      )
      // Fix relative paths for new location
      .pipe(gulpReplace("../../../Build", "../../.."))
      .pipe(gulpReplace("../../Source", "../../../Source"))
      .pipe(gulpReplace("../../SampleData", "../../../../Apps/SampleData"))
      .pipe(gulpReplace("Build/Docs", "Docs"))
      .pipe(gulp.dest("Build/Apps/Sandcastle"));
  }
  streams.push(appStream);

  let imageStream = gulp.src(
    ["Apps/Sandcastle/gallery/**.jpg", "Apps/Sandcastle/images/**"],
    {
      base: "Apps/Sandcastle",
      encoding: false,
    }
  );
  if (isProduction) {
    imageStream = imageStream.pipe(gulp.dest("Build/Sandcastle"));
  } else {
    imageStream = imageStream.pipe(gulp.dest("Build/Apps/Sandcastle"));
  }
  streams.push(imageStream);

  if (isProduction) {
    const dataStream = gulp
      .src(["Apps/SampleData/**"], { encoding: false })
      .pipe(gulp.dest("Build/Sandcastle/SampleData"));
    streams.push(dataStream);
  }

  const standaloneStream = gulp
    .src(["Apps/Sandcastle/standalone.html"])
    .pipe(gulpReplace("../../../", "."))
    .pipe(
      gulpReplace(
        '    <script type="module" src="load-barium-es6.js"></script>',
        '    <script src="../BariumUnminified/Barium.js"></script>\n' +
          '    <script>window.BARIUM_BASE_URL = "../BariumUnminified/";</script>'
      )
    )
    .pipe(gulpReplace("../../Build", "."))
    .pipe(gulp.dest("Build/Sandcastle"));
  streams.push(standaloneStream);

  return Promise.all(streams.map((s) => finished(s)));
}

async function buildBariumViewer() {
  const BARIUMViewerOutputDirectory = isProduction
    ? "Build/BariumViewer"
    : "Build/Apps/BariumViewer";
  mkdirp.sync(BARIUMViewerOutputDirectory);

  const config = defaultESBuildOptions();
  config.entryPoints = [
    "Apps/BariumViewer/BariumViewer.js",
    "Apps/BariumViewer/BariumViewer.css",
  ];
  config.bundle = true; // Tree-shaking is enabled automatically
  config.minify = true;
  config.loader = {
    ".gif": "text",
    ".png": "text",
  };
  config.format = "iife";
  // Configure Barium base path to use built
  config.define = { BARIUM_BASE_URL: `"."` };
  config.outdir = BARIUMViewerOutputDirectory;
  config.outbase = "Apps/BariumViewer";
  config.logLevel = "error"; // print errors immediately, and collect warnings so we can filter out known ones
  const result = await esbuild(config);

  handleBuildWarnings(result);

  await esbuild({
    entryPoints: ["packages/widgets/Source/InfoBox/InfoBoxDescription.css"],
    minify: true,
    bundle: true,
    loader: {
      ".gif": "text",
      ".png": "text",
    },
    outdir: join(BARIUMViewerOutputDirectory, "Widgets"),
    outbase: "packages/widgets/Source/",
  });

  await bundleWorkers({
    minify: true,
    removePragmas: true,
    path: BARIUMViewerOutputDirectory,
  });

  const stream = gulp
    .src(
      [
        "Apps/BariumViewer/**",
        "!Apps/BariumViewer/Images",
        "!Apps/BariumViewer/**/*.js",
        "!Apps/BariumViewer/**/*.css",
      ],
      {
        encoding: false,
      }
    )
    .pipe(
      gulp.src(
        [
          "Build/Barium/Assets/**",
          "Build/Barium/Workers/**",
          "Build/Barium/Widgets/**",
          "!Build/Barium/Widgets/**/*.css",
        ],
        {
          base: "Build/Barium",
          nodir: true,
          encoding: false,
        }
      )
    )
    .pipe(gulp.src(["web.config"]))
    .pipe(gulp.dest(BARIUMViewerOutputDirectory));

  await finished(stream);
  return stream;
}
